classdef dotsTheMessenger < dotsAllSingletonObjects
    % @class dotsTheMessenger
    % Singleton to communicate between different machines
    
    properties
        % topsGroupedList of info about opened sockets
        socketsInfo;
        
        clockFunction;
        waitFunction;
        messageTimeout;

        socketClassname;
        socketObject;

        defaultClientIP;
        defaultServerIP;
        defaultClientPort;
        defaultServerPort;
    end
    
    methods (Access = private)
        function self = dotsTheMessenger
            self.initialize;
        end
    end
    
    methods (Static)
        function m = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                self = dotsTheMessenger;
            end
            m = self;
        end
        
        function reset
            self = dotsTheMessenger.theObject;
            self.initialize;
        end
        
        function g = gui
            self = dotsTheMessenger.theObject;
            g = self.socketsInfo.gui;
        end
    end
    
    methods
        function initialize(self)
            if isobject(self.socketObject)
                self.socketObject.closeAll;
            end
            self.initializeLists({'socketsInfo'});
            
            dotsTheMachineConfiguration.applySettingsToObject(self);
            dotsTheSwitchboard.sharePropertyOfObject('clockFunction', self);
            dotsTheSwitchboard.sharePropertyOfObject('waitFunction', self);
            dotsTheSwitchboard.sharePropertyOfObject('messageTimeout', self);

            self.socketObject = feval(self.socketClassname);
            self.socketObject.closeAll;
        end
        
        % Send a message from the given socket
        % @param msg a string message to send
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @details
        % Sends a message from @a sock, to whatever address and port were
        % specified in the call to openSocket();
        % @details
        % Returns a status code.  A negative status code indicates an error
        % and probably failure to serialize or send @a msg.
        function status = sendMessageFromSocket(self, msg, sock)
            %disp(sprintf('sent from %d:', sock)),msg
            [bytes, status] = mxGram('mxToBytes', msg);
            if status >= 0
                status = self.socketObject.writeBytes(sock, bytes);
            end
            % if status < 0
            %     status
            %     size(msg)
            %     class(msg)
            %     error('failed to send msg')
            % end
        end

        % Receive any message that arrived at the given socket
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @details
        % If a message has arrived at @a sock, returns the message
        % variable. Otherwise, returns an empty array.
        % @details
        % Returns a status code.  A negative status code indicates an error
        % and probably failure to decode @a msg from bytes to a variable.
        function [msg, status] = receiveMessageAtSocket(self, sock)
            bytes = self.socketObject.readBytes(sock);
            if isa(bytes, 'uint8')
                [msg, status] = mxGram('bytesToMx', bytes);
            end
            %disp(sprintf('received at %d:', sock)),msg
        end
        
        % Block and check the given socket an arriving message
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @param duration optional number of seconds to wait for a message
        % to arrive.  Defaults to the value of dotsTheMessenger's
        % messageTimeout property.
        % @details
        % Blocks for the specified duration, checking for messages arriving
        % at @a sock and yielding CPU cycles with the function given in
        % dotsTheMessenger's waitFunction property.
        % @details
        % If a message arrives at @a sock during the specified interval (or
        % had already arrived), returns the message variable immediately.
        % If no message arrives, returns an empty array after waiting the
        % full interval.
        % @details
        % Returns a status code.  A negative status code indicates an error
        % and probably failure to decode @a msg from bytes to a variable.
        function [msg, status] = waitForMessageAtSocket(self, sock, duration)
            if nargin < 3
                duration = self.messageTimeout;
            end
            cf = self.clockFunction;
            wf = self.waitFunction;
            endTime = feval(cf) + duration;
            
            gotMsg = self.socketObject.check(sock);
            while gotMsg==0 && feval(cf) < endTime
                feval(wf);
                gotMsg = self.socketObject.check(sock);
            end
            [msg, status] = self.receiveMessageAtSocket(sock);
            %disp(sprintf('arrived at %d:', sock)),msg
        end
        
        % Open a socket for communicating via Ethernet and UDP
        % @param localIP string IP address for this machine
        % @param localPort integer port number to go with the local IP
        % address
        % @param remoteIP string IP address for this or another machine
        % @param remotePort integer port number to go with the remote IP
        % address
        % @details
        % Opens an Ethernet UDP connection between ports at two IP
        % addresses.  The IP addresses may be the same or different.  @a
        % localIP should be the address of this machine, and may be the
        % "loopback" address, '127.0.0.1'.  The port numbers must be
        % different.
        % @details
        % Returns a numeric identifier for the new UDP connection, suitable
        % for use with sendMessageFromSocket(), receiveMessageAtSocket(),
        % and waitForMessageAtSocket();
        % @details
        % Stores information about each opened socket in socketsInfo
        function sock = openSocket(self, localIP, localPort, remoteIP, remotePort)
            sock = self.socketObject.open( ...
                localIP, localPort, ...
                remoteIP, remotePort);
            
            if sock >= 0
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    localIP, sock, 'local IP')
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    remoteIP, sock, 'remote IP')
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    localPort, sock, 'local port')
                self.socketsInfo.addItemToGroupWithMnemonic( ...
                    remotePort, sock, 'remote port')
            end
            
            % disp(sprintf('%s, %d, %s, %d ...%d', ...
            %     localIP, localPort, remoteIP, remotePort, sock));
        end
        
        % Close a socket 
        % @param sock a numeric identifier for a socket, as returned by
        % openSocket()
        % @details
        % Frees the resources associeted with @a sock.
        % @details
        % Returns a status code.  A negative status code probably indicates
        % that @a sock is not a valid socket identifier.
        function status = closeSocket(self, sock)
            if isempty(sock)
                status = 0;
            else
                status = self.socketObject.close(sock);
            end
        end

        function sock = openDefaultServerSocket(self)
            sock = self.openSocket( ...
                self.defaultServerIP, self.defaultServerPort, ...
                self.defaultClientIP, self.defaultClientPort);
        end
        
        function sock = openDefaultClientSocket(self)
            sock = self.openSocket( ...
                self.defaultClientIP, self.defaultClientPort, ...
                self.defaultServerIP, self.defaultServerPort);
        end
    end
end